菜单管理:使用el-tree组件展示默认菜单结构
概述
菜单管理是后台管理系统的核心模块之一。本节介绍如何使用 Element Plus 的 el-tree 组件展示系统菜单的层级结构,实现菜单数据的可视化管理。菜单数据与路由配置紧密关联,前端菜单管理页面将作为后台角色权限配置的可视化入口。
菜单管理的业务背景
为什么需要菜单管理页面
在前后端分离的权限体系中,菜单管理承担以下职责:
| 职责 | 说明 |
|---|---|
| 数据可视化 | 以树形结构直观展示所有菜单及其层级关系 |
| 权限配置 | 控制哪些菜单对哪些角色可见 |
| 接口联动 | 前端菜单数据与后端角色/权限数据打通 |
| 动态控制 | 支持菜单名称修改、排序、显隐控制等 |
菜单数据与路由的关系
项目中已实现的路由逻辑包含两个关键环节:
defaults.view中的菜单过滤 — 根据服务端返回的用户权限数据,过滤掉无权访问的菜单router中的路由守卫 — 判断目标页面是否在用户权限范围内,未授权则跳转登录页
用户访问页面 → router守卫检查权限 → 无权限则跳转登录 → 有权限则加载菜单
↓
defaults.view 过滤菜单数据
text
allRoots 管理员权限标识
为方便开发调试,在 user store 中引入了 allRoots 属性:
// stores/user.ts
interface UserState {
allRoots: boolean // 管理员标识,true 表示拥有所有菜单访问权限
// ...其他用户状态
}
typescript
当 allRoots 为 true 时,defaults.view 直接返回 generatedMenuData(完整的菜单数据),不再进行服务端过滤,使开发者可以访问系统中的所有页面。
使用 el-tree 展示菜单结构
el-tree 核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
data | array | 树形数据源 |
node-key | string | 节点唯一标识字段名 |
props | object | 数据字段映射配置 |
default-expand-all | boolean | 是否默认展开所有节点 |
show-checkbox | boolean | 是否显示复选框 |
highlight-current | boolean | 是否高亮当前选中节点 |
expand-on-click-node | boolean | 点击节点是否展开/收起 |
基础实现
<template>
<div class="menu-management">
<el-tree
ref="treeRef"
:data="menuData"
node-key="id"
:props="treeProps"
default-expand-all
highlight-current
:expand-on-click-node="false"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<span>{{ node.label }}</span>
<span class="node-meta">
<el-tag v-if="data.meta?.icon" size="small" type="info">
{{ data.meta.icon }}
</el-tag>
</span>
</div>
</template>
</el-tree>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { TreeInstance } from 'element-plus'
interface MenuItem {
id: string
label: string
children?: MenuItem[]
meta?: {
icon?: string
title?: string
hidden?: boolean
}
path?: string
}
const treeRef = ref<TreeInstance>()
const treeProps = {
children: 'children',
label: 'label'
}
// 菜单数据来源于 generatedMenuData
const menuData = ref<MenuItem[]>([
{
id: 'dashboard',
label: '仪表盘',
path: '/dashboard',
meta: { icon: 'Dashboard' },
children: []
},
{
id: 'system',
label: '系统管理',
path: '/system',
meta: { icon: 'Setting' },
children: [
{
id: 'system-user',
label: '用户管理',
path: '/system/user',
meta: { icon: 'User' }
},
{
id: 'system-menu',
label: '菜单管理',
path: '/system/menu',
meta: { icon: 'Menu' }
}
]
}
])
</script>
<style scoped>
.menu-management {
padding: 16px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.node-meta {
display: flex;
gap: 4px;
}
</style>
vue
菜单数据转换
路由配置数据需要转换为 el-tree 所需的树形结构:
// utils/menu.ts
import type { RouteRecordRaw } from 'vue-router'
interface TreeNode {
id: string
label: string
children?: TreeNode[]
path: string
meta?: Record<string, unknown>
}
/**
* 将路由配置转换为树形数据
* el-tree 要求 data 字段为树形嵌套结构
*/
export function transformRoutesToTree(routes: RouteRecordRaw[]): TreeNode[] {
return routes
.filter(route => !route.meta?.hidden)
.map(route => ({
id: route.name as string || route.path,
label: (route.meta?.title as string) || route.path,
path: route.path,
meta: route.meta,
children: route.children
? transformRoutesToTree(route.children)
: undefined
}))
}
typescript
el-tree props 配置映射
props 属性用于指定数据字段与树节点属性的映射关系:
// 当后端数据字段与 el-tree 默认字段不一致时
const treeProps = {
children: 'children', // 子节点字段名
label: 'name', // 显示文本字段名(对应后端的 name 字段)
disabled: 'disabled', // 禁用状态字段名
isLeaf: 'isLeaf' // 是否为叶子节点字段名
}
typescript
关键注意事项
| 问题 | 解决方案 |
|---|---|
default-expanded-keys 不生效 | 必须同时设置 node-key,且 key 值在整个树中唯一 |
| 自定义节点内容 | 使用 #default 作用域插槽或 render-content 属性 |
| 数据更新后树不刷新 | 确保使用响应式数据源,或通过 treeRef.value?.store 手动更新 |
| 大量节点性能问题 | 设置 render-after-expand 为 true(默认值),懒加载子节点 |
实践要点
- 菜单管理页面用于管理左侧路由数据的显示和访问权限
- 通过
allRoots标识可临时获取管理员权限,方便调试 el-tree的node-key必须设置为节点数据中的唯一字段- 使用作用域插槽
#default="{ node, data }"自定义节点渲染 - 路由配置转换为树形数据时需过滤
hidden路由并映射meta.title到label
↑